Išmokite optimizuoti React ref atgalinį ryšį. Sužinokite, kodėl suveikia dukart, kaip išvengti to su useCallback, ir pagerinkite programų našumą.
React ref atgalinio ryšio išmokimas: išsamus našumo optimizavimo vadovas
Šiuolaikiniame žiniatinklio kūrimo pasaulyje našumas yra ne tik savybė; tai būtinybė. Kūrėjams, naudojantiems React, greitų, reaguojančių vartotojo sąsajų kūrimas yra pagrindinis tikslas. Nors React virtualus DOM ir sutaikymo algoritmas atlieka didžiąją dalį sunkaus darbo, yra specifinių šablonų ir API, kuriems gilus supratimas yra labai svarbus, norint pasiekti maksimalų našumą. Viena tokių sričių yra nuorodų valdymas, ypač dažnai neteisingai suprantamas atgalinio ryšio refų elgesys.
Refai suteikia būdą pasiekti DOM mazgus arba React elementus, sukurtus renderinimo metode – tai esminė išeitis tokioms užduotims kaip fokusavimo valdymas, animacijų inicijavimas ar integravimas su trečiųjų šalių DOM bibliotekomis. Nors useRef tapo standartu paprastiems atvejams funkciniuose komponentuose, atgalinio ryšio refai suteikia galingesnę, tikslesnę kontrolę, kada nuoroda nustatoma ir panaikinama. Tačiau ši galia turi ir subtilybę: atgalinio ryšio refas gali suveikti kelis kartus komponento gyvavimo ciklo metu, o tai gali sukelti našumo problemas ir klaidas, jei nėra tinkamai valdomas.
Šis išsamus vadovas išsklaidys React ref atgalinio ryšio paslaptis. Apžvelgsime:
- Kas yra atgalinio ryšio refai ir kuo jie skiriasi nuo kitų refų tipų.
- Pagrindinę priežastį, kodėl atgalinio ryšio refai iškviečiami du kartus (vieną kartą su
null, ir vieną kartą su elementu). - Našumo spąstus, naudojant įterptąsias funkcijas ref atgalinio ryšio funkcijoms.
- Galutinį optimizavimo sprendimą naudojant
useCallback„hook‘ą“. - Pažangius šablonus priklausomybėms tvarkyti ir integravimui su išorinėmis bibliotekomis.
Šio straipsnio pabaigoje turėsite žinių, kad galėtumėte užtikrintai naudoti atgalinio ryšio refus, užtikrindami, jog jūsų React programos būtų ne tik patikimos, bet ir itin našios.
Trumpas priminimas: Kas yra atgalinio ryšio refai?
Prieš pasinerdami į optimizavimą, trumpai prisiminkime, kas yra atgalinio ryšio refas. Vietoj to, kad perduotumėte ref objektą, sukurtą useRef() arba React.createRef(), ref atributui perduodate funkciją. Šią funkciją React vykdo, kai komponentas prijungiamas ir atjungiamas.
React iškvies ref atgalinio ryšio funkciją su DOM elementu kaip argumentu, kai komponentas prisijungs, ir iškvies jį su null kaip argumentu, kai komponentas atsijungs. Tai suteikia jums tikslią kontrolę tiksliais momentais, kai nuoroda tampa prieinama arba bus sunaikinta.
Štai paprastas pavyzdys funkciniame komponente:
import React, { useState } from 'react';
function TextInputWithFocusButton() {
let textInput = null;
const setTextInputRef = element => {
console.log('Ref callback fired with:', element);
textInput = element;
};
const focusTextInput = () => {
// Focus the text input using the raw DOM API
if (textInput) textInput.focus();
};
return (
<div>
<input type="text" ref={setTextInputRef} />
<button onClick={focusTextInput}>
Focus the text input
</button>
</div>
);
}
Šiame pavyzdyje setTextInputRef yra mūsų atgalinio ryšio refas. Jis bus iškviestas su <input> elementu, kai bus atvaizduotas, leisdamas mums jį išsaugoti ir vėliau naudoti focus() iškvietimui.
Pagrindinė problema: Kodėl ref atgalinio ryšio funkcijos suveikia du kartus?
Pagrindinis elgesys, kuris dažnai glumina kūrėjus, yra dvigubas atgalinio ryšio funkcijos iškvietimas. Kai komponentas su atgalinio ryšio refu atvaizduojamas, atgalinio ryšio funkcija paprastai iškviečiama du kartus iš eilės:
- Pirmas iškvietimas: su
nullkaip argumentu. - Antras iškvietimas: su DOM elemento egzemplioriumi kaip argumentu.
Tai nėra klaida; tai apgalvotas React komandos dizaino sprendimas. Iškvietimas su null reiškia, kad ankstesnis refas (jei toks buvo) atjungiamas. Tai suteikia jums gyvybiškai svarbią galimybę atlikti valymo operacijas. Pavyzdžiui, jei ankstesniame atvaizdavime prie mazgo prijungėte įvykių klausiklį, null iškvietimas yra puikus momentas jį pašalinti prieš prijungiant naują mazgą.
Tačiau problema yra ne šis prijungimo/atjungimo ciklas. Tikroji našumo problema atsiranda, kai šis dvigubas suveikimas įvyksta kiekvieną kartą persirendinant, net jei komponento būsena atnaujinama visiškai nesusijusiu su pačiu refu būdu.
Įterptųjų funkcijų spąstai
Apsvarstykite šią, atrodo, nekaltai atrodančią, įgyvendinimo strategiją funkcinio komponento viduje, kuris persirenderina:
import React, { useState } from 'react';
function FrequentUpdatesComponent() {
const [count, setCount] = useState(0);
return (
<div>
<h3>Counter: {count}</h3>
<button onClick={() => setCount(c => c + 1)}>Increment</button>
<div
ref={(node) => {
// This is an inline function!
console.log('Ref callback fired with:', node);
}}
>
I am the referenced element.
</div>
</div>
);
}
Jei paleisite šį kodą ir paspausite mygtuką "Increment", kiekvieną kartą paspaudus konsolėje matysite štai ką:
Ref callback fired with: null
Ref callback fired with: <div>...</div>
Kodėl taip atsitinka? Nes kiekvieno renderinimo metu sukuriate visiškai naują funkcijos egzempliorių ref propui: (node) => { ... }. Rekonciliacijos proceso metu React palygina ankstesnio renderinimo propus su dabartiniais. Jis mato, kad ref propas pasikeitė (iš seno funkcijos egzemplioriaus į naują). React sutartis yra aiški: jei ref atgalinio ryšio funkcija pasikeičia, ji pirmiausia turi išvalyti seną refą, iškviesdama jį su null, o tada nustatyti naują, iškviesdama jį su DOM mazgu. Tai nereikalingai suaktyvina valymo/nustatymo ciklą kiekviename renderinime.
Paprastam console.log tai yra nedidelis našumo nuostolis. Bet įsivaizduokite, kad jūsų atgalinio ryšio funkcija atlieka kažką brangaus:
- Sunkiai atliekamų įvykių klausiklių prijungimas ir atjungimas (pvz., `scroll`, `resize`).
- Sunkios trečiosios šalies bibliotekos inicijavimas (pvz., D3.js diagramos arba žemėlapių bibliotekos).
- DOM matavimų atlikimas, sukeliantis išdėstymo perskaičiavimą.
Šios logikos vykdymas kiekvieno būsenos atnaujinimo metu gali stipriai pabloginti jūsų programos našumą ir įdiegti subtilių, sunkiai aptinkamų klaidų.
Sprendimas: atminties išsaugojimas su `useCallback`
Šios problemos sprendimas yra užtikrinti, kad React gautų visiškai tą patį funkcijos egzempliorių ref atgalinio ryšio funkcijai per visus persirenderinimus, nebent mes aiškiai norime, kad ji pasikeistų. Tai puikus atvejis naudoti useCallback „hook‘ą“.
useCallback grąžina atmintyje išsaugotą atgalinio ryšio funkcijos versiją. Ši atmintyje išsaugota versija keičiasi tik tada, jei pasikeičia viena iš jos priklausomybių masyvo priklausomybių. Pateikdami tuščią priklausomybių masyvą ([]), galime sukurti stabilią funkciją, kuri išlieka visą komponento gyvavimo laiką.
Perdirbkime mūsų ankstesnį pavyzdį naudodami useCallback:
import React, { useState, useCallback } from 'react';
function OptimizedComponent() {
const [count, setCount] = useState(0);
// Create a stable callback function with useCallback
const myRefCallback = useCallback(node => {
// This logic now runs only when the component mounts and unmounts
console.log('Ref callback fired with:', node);
if (node !== null) {
// You can perform setup logic here
console.log('Element is mounted!');
}
}, []); // <-- Empty dependency array means the function is created only once
return (
<div>
<h3>Counter: {count}</h3>
<button onClick={() => setCount(c => c + 1)}>Increment</button>
<div ref={myRefCallback}>
I am the referenced element.
</div>
</div>
);
}
Dabar, paleidus šią optimizuotą versiją, konsolės pranešimą matysite tik du kartus iš viso:
- Vieną kartą, kai komponentas iš pradžių prijungiamas (
Ref callback fired with: <div>...</div>). - Vieną kartą, kai komponentas atsijungia (
Ref callback fired with: null).
Paspaudus mygtuką "Increment", ref atgalinio ryšio funkcija nebebus suaktyvinama. Sėkmingai užkirtome kelią nereikalingam valymo/nustatymo ciklui kiekvieno persirenderinimo metu. React mato tą patį funkcijos egzempliorių ref propui vėlesniuose renderinimuose ir teisingai nustato, kad jokie pakeitimai nereikalingi.
Pažangūs scenarijai ir geriausia praktika
Priklausomybių tvarkymas atgalinio ryšio funkcijoje
Įsivaizduokite, kad jums reikia paleisti tam tikrą logiką ref atgalinio ryšio funkcijoje, kuri priklauso nuo būsenos dalies arba propso. Pavyzdžiui, nustatyti `data-` atributą pagal dabartinę temą.
function ThemedComponent({ theme }) {
const [internalState, setInternalState] = useState(0);
const themedRefCallback = useCallback(node => {
if (node !== null) {
// This callback now depends on the 'theme' prop
console.log(`Setting theme attribute to: ${theme}`);
node.setAttribute('data-theme', theme);
}
}, [theme]); // <-- Add 'theme' to the dependency array
return (
<div>
<p>Current Theme: {theme}</p>
<div ref={themedRefCallback}>This element's theme will update.</div>
{/* ... imagine a button here to change the parent's theme ... */}
</div>
);
}
Šiame pavyzdyje mes pridėjome theme prie useCallback priklausomybių masyvo. Tai reiškia:
- Nauja
themedRefCallbackfunkcija bus sukurta tik tada, kai pasikeisthemepropsas. - Kai pasikeičia
themepropsas, React aptinka naują funkcijos egzempliorių ir pakartotinai paleidžia ref atgalinio ryšio funkciją (pirmiausia sunull, tada su elementu). - Tai leidžia mūsų efektui – `data-theme` atributo nustatymui – vėl paleisti su atnaujinta
themereikšme.
Tai yra teisingas ir numatytas elgesys. Mes aiškiai sakome React, kad pakartotinai suaktyvintų ref logiką, kai pasikeičia jos priklausomybės, tuo pačiu užkertant kelią jos veikimui, kai atnaujinamos nesusijusios būsenos.
Integracija su trečiųjų šalių bibliotekomis
Vienas galingiausių atgalinio ryšio refų naudojimo atvejų yra trečiųjų šalių bibliotekų egzempliorių, kuriuos reikia prijungti prie DOM mazgo, inicijavimas ir sunaikinimas. Šis šablonas puikiai išnaudoja atgalinio ryšio funkcijos prijungimo/atjungimo pobūdį.
Štai patikimas šablonas bibliotekos, tokios kaip diagramų ar žemėlapių biblioteka, valdymui:
import React, { useRef, useCallback, useEffect } from 'react';
import SomeChartingLibrary from 'some-charting-library';
function ChartComponent({ data }) {
// Use a ref to hold the library instance, not the DOM node
const chartInstance = useRef(null);
const chartContainerRef = useCallback(node => {
// The node is null when the component unmounts
if (node === null) {
if (chartInstance.current) {
console.log('Cleaning up chart instance...');
chartInstance.current.destroy(); // Cleanup method from the library
chartInstance.current = null;
}
return;
}
// The node exists, so we can initialize our chart
console.log('Initializing chart instance...');
const chart = new SomeChartingLibrary(node, {
// Configuration options
data: data,
});
chartInstance.current = chart;
}, [data]); // Re-create the chart if the data prop changes
return <div className="chart-container" ref={chartContainerRef} style={{ height: '400px' }} />;
}
Šis šablonas yra išskirtinai tvarkingas ir atsparus:
- Inicijavimas: Kai `div` prisijungia, atgalinio ryšio funkcija gauna `node`. Ji sukuria naują diagramų bibliotekos egzempliorių ir išsaugo jį `chartInstance.current`.
- Valymas: Kai komponentas atsijungia (arba jei `data` pasikeičia, suaktyvinant pakartotinį paleidimą), atgalinio ryšio funkcija pirmiausia iškviečiama su `null`. Kodas patikrina, ar egzistuoja diagramos egzempliorius ir, jei taip, iškviečia jo `destroy()` metodą, užkertant kelią atminties nuotėkiams.
- Atnaujinimai: Įtraukdami `data` į priklausomybių masyvą, užtikriname, kad jei diagramos duomenys turi būti iš esmės pakeisti, visa diagrama būtų švariai sunaikinta ir iš naujo inicijuota su naujais duomenimis. Paprastiems duomenų atnaujinimams biblioteka gali pasiūlyti `update()` metodą, kuris galėtų būti tvarkomas atskirame `useEffect`.
Našumo palyginimas: Kada optimizavimas *tikrai* svarbus?
Nežymus poveikis scenarijuose
Jei jūsų atgalinio ryšio funkcija atlieka tik paprastą kintamojo priskyrimą, naujos funkcijos kūrimo kaštai kiekviename renderinime yra minimalūs. Šiuolaikinės JavaScript varikliai yra neįtikėtinai greiti funkcijų kūrimo ir šiukšlių rinkimo atžvilgiu.
Pavyzdys: ref={(node) => (myRef.current = node)}
Tokiais atvejais, nors techniškai mažiau optimalu, vargu ar kada nors pastebėsite našumo skirtumą realaus pasaulio programoje. Nepakliūkite į per ankstyvo optimizavimo spąstus.
Reikšmingo poveikio scenarijai
Visada turėtumėte naudoti useCallback, kai jūsų ref atgalinio ryšio funkcija atlieka bet kurią iš šių veiksmų:
- DOM manipuliacija: Tiesioginis klasių pridėjimas ar pašalinimas, atributų nustatymas ar elementų dydžių matavimas (kurie gali sukelti išdėstymo perskaičiavimą).
- Įvykių klausikliai: `addEventListener` ir `removeEventListener` iškvietimas. Tai vykdant kiekviename renderinime garantuotai atsiranda klaidų ir našumo problemų.
- Bibliotekos inicijavimas: Kaip parodyta mūsų diagramų pavyzdyje, sudėtingų objektų inicijavimas ir sunaikinimas yra brangus.
- Tinklo užklausos: API iškvietimo atlikimas, pagrįstas DOM elemento egzistavimu.
- Refų perdavimas atmintyje išsaugotų vaikų komponentams: Jei perduodate ref atgalinio ryšio funkciją kaip propą vaiko komponentui, apvyniotam
React.memo, nestabili įterptoji funkcija sulaužys atminties išsaugojimą ir privers vaiką nereikalingai persirenderinti.
Gera nykščio taisyklė: Jei jūsų ref atgalinio ryšio funkcija turi daugiau nei vieną, paprastą priskyrimą, išsaugokite ją atmintyje naudodami useCallback.
Išvada: Nuspėjamo ir našaus kodo rašymas
React ref atgalinio ryšio funkcija yra galingas įrankis, suteikiantis tikslų DOM mazgų ir komponentų egzempliorių valdymą. Jo gyvavimo ciklo – ypač tyčinio `null` iškvietimo valymo metu – supratimas yra raktas į efektyvų jo naudojimą.
Sužinojome, kad įprastas klaidingas šablonas, kai naudojama įterptoji funkcija ref propui, veda prie nereikalingų ir potencialiai brangių pakartotinių vykdymų kiekvieno renderinimo metu. Sprendimas yra elegantiškas ir būdingas React: stabilizuoti atgalinio ryšio funkciją naudojant useCallback „hook‘ą“.
Įvaldę šį šabloną, galite:
- Užkirsti kelią našumo problemoms: Išvengti brangios sąrankos ir išardymo logikos kiekvieno būsenos pakeitimo metu.
- Pašalinti klaidas: Užtikrinti, kad įvykių klausikliai ir bibliotekų egzemplioriai būtų tvarkomi švariai, be dublikatų ar atminties nuotėkių.
- Rašyti nuspėjamą kodą: Kurti komponentus, kurių ref logika veikia tiksliai taip, kaip tikimasi, vykdoma tik tada, kai komponentas prijungiamas, atjungiamas arba kai pasikeičia jo specifinės priklausomybės.
Kitą kartą, kai pasieksite refą, kad išspręstumėte sudėtingą problemą, prisiminkite atmintyje išsaugoto atgalinio ryšio galią. Tai nedidelis jūsų kodo pakeitimas, kuris gali žymiai pakeisti jūsų React programų kokybę ir našumą, prisidedant prie geresnės patirties vartotojams visame pasaulyje.